import { Reflector } from '@nestjs/core'
import { JwtService } from '@nestjs/jwt'
import { ConfigService } from '@nestjs/config'
import { RedisService } from '@/shared/redis.service'
import { ApiException } from '../exception/api.exception'
import { DecoratorEnum, CacheKey, AuthEnum, ConfigEnum, TimeUnit } from '..'
import { CanActivate, ExecutionContext, HttpStatus, Injectable, Logger } from '@nestjs/common'

@Injectable()
export class JwtAuthGuard implements CanActivate {
  private readonly logger = new Logger(JwtAuthGuard.name)

  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
    private readonly redisService: RedisService,
    private readonly configService: ConfigService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 通过 ExecutionContext 获取 HTTP 请求对象
    const request = context.switchToHttp().getRequest<ExpressRequest>()

    // 检查当前请求是否标记为公共路由，如果是，则无需 Token 验证
    const isPublic = this.reflector.getAllAndOverride<boolean>(DecoratorEnum.PUBLIC_METADATA, [context.getHandler(), context.getClass()]) ?? false
    if (isPublic) return true

    // 此处可添加一些全局安全校验，比如参数加密、时间戳之类

    // 校验是否需要校验 Token
    const allowNoToken = this.reflector.getAllAndOverride<boolean>(DecoratorEnum.ALLOW_NO_TOKEN, [context.getHandler(), context.getClass()]) ?? false
    if (allowNoToken) return true

    // 从请求头中提取 Bearer Token
    const token = this._extractTokenFromHeader(request)
    if (!token) throw new ApiException('无效的访问凭证', HttpStatus.UNAUTHORIZED)

    // try catch 是为了统一返回的错误消息 语义化提示
    try {
      // 对请求头携带的 Token 进行校验
      const payload = this.jwtService.verify<JwtPayload>(token)
      const REDIS_TOKEN_KEY = `${CacheKey.ADMIN_USER_TOKEN}:${payload.userId}`
      const expiresIn = this.configService.get<number>(ConfigEnum.JWT_EXPIRESIN)!
      const redisToken = await this.redisService.getEx(REDIS_TOKEN_KEY, expiresIn)
      if (!redisToken) throw new Error('不存在的访问凭证')
      request[AuthEnum.JWT_PAYLOAD] = payload
      // 续期在线用户时长
      await this.redisService.expire(`${CacheKey.ADMIN_USER_ONLINE}:${payload.userId}`, TimeUnit.HALF_HOUR)
      return true
    } catch (error: any) {
      this.logger.error(error)
      let message = error.message ?? '认证失败，请重新登录'
      if (['invalid token', 'invalid signature'].includes(message)) message = '无效的访问凭证'
      throw new ApiException(error.message ?? '认证失败，请重新登录', HttpStatus.UNAUTHORIZED)
    }
  }

  /** 从请求头中提取 Bearer Token */
  private _extractTokenFromHeader(request: ExpressRequest): string | undefined {
    const [type, token] = request.headers[AuthEnum.AUTHORIZATION]?.split(' ') ?? []
    return type === AuthEnum.TOKEN_PREFIX ? token : undefined
  }
}
